🐦 BIRD MIGRATION APP v.2.02 🐦
imports¶
# -.-|m { input: true, input_fold: hide}
#plotting
import matplotlib.pyplot as plt
#dataframe stuff
import numpy as np
import pandas as pd
#a bunch of other shit i need
import re, os,random
#geo stuff
import cartopy.crs as ccrs
import geopandas as gpd
#panel & bokeh stuff
import holoviews as hv
import hvplot
from hvplot import pandas
import panel as pn
import bokeh.io
from panel.io import hold
from bokeh.themes.theme import Theme
bokeh.io.output_notebook();
pn.extension('mathjax')
hv.extension('bokeh');
#font
from matplotlib import font_manager
from markdown import markdown
from IPython.display import HTML
#ipython
from IPython.display import clear_output
clear_output()
# -.-|m { input: true, input_fold: hide}
#custom packages. IMPORT SEPRATELY DUE TO RATE LIMITNG.
import birdspecies as bs #birdspecies.py
import color_swag as cswag #color_swag.py
GLOBALS¶
grabbing package items¶
url = bs.url
world = bs.world
bf = bs.bf
allspecies = bs.species
sp_info = bs.c_status
additional global variables¶
#GLOBAL MARKDOWN TABLE FORMAT
#SO I DO NOT HAVE TO DO I/O EVERYTIME THE PLOT UPDATES.
f = open("legendtemplate.txt", "r")
GLOBAL_TABLE_MARKDOWN = f.read()
f.close()
#monthz
GLOBAL_MONTHZ = {
1:"January",
2:"Febuary",
3:"March",
4:"April",
5:"May",
6:"June",
7:"July",
8:"August",
9:"September",
10:"October",
11:"November",
12:"December"
}
global css & style stuff¶
GLOBAL_PINK = "#FF69B4"
font_manager.fontManager.addfont(os.path.join("fonts", "Delius-Regular.ttf"))
theme = Theme(
json={
'attrs' : {
'Text':
{
'text_font': 'Delius',
},
'Title': {
'text_font': 'Delius',
'text_color': '#FF69B4',
},
}
})
hv.renderer('bokeh').theme = theme
pn.config.raw_css.append("""
@font-face {
font-family: Delius;
}
.sidebar {
background: linear-gradient(135deg, #FF9EB3, #FFF954);
border-radius: 12px;
padding: 5px;
margin-top: 10px;
margin-bottom: 10px;
margin-right: 10px;
color: #D88CF8 !important;
box-shadow: 0 0 10px rgba(245, 40, 145, 0.8);
font-family: Delius;
--bokeh-mono-font: Delius;
}
.legendbox{
background: linear-gradient(135deg, #FF9EB3, #FFF954);
border-radius: 12px;
padding: 5px;
margin-top: 10px;
margin-bottom: 10px;
margin-right: 10px;
color: #D88CF8 !important;
box-shadow: 0 0 10px rgba(245, 40, 145, 0.8);
font-family: Delius;
--bokeh-mono-font: Delius;
}
.blurb {
font-size: 18px;
line-height: 1.2em;
}
.swag {
background-color: #FFFFFF;
box-shadow: 0 0 10px rgba(245, 40, 145, 0.8);
}
.birb {
fill: #D88CF8 !important;
}
""")
#WELCOME TO THIS INCREDIBLY COOKED SOLUTION.
#so this makes a dictionary assigning a random pastel color for each bird species for
#the world plot polygons when it is rendered. thats cuz i wanted colors to change by bird species
#but not individual data series plotting and also ididnt feel like asinging specific birds
GLOBAL_PLTCOLORZ = {allspecies[i]:random.choice(cswag.pastels) for i in range(len(allspecies))}
computational¶
route calculater¶
def route(species, c):
"""gets all the routes for this bird species and when he traveled."""
#THIS NEEDS TO BE LESS COOKED. i litrlyy dont think it needs the species argument at all
#rework when i get the chance.
locs = bf[(bf['Bird species'] == species) & (bf['Migratory route codes'] == c)]
return [locs["GPS_xx"].tolist(), locs["GPS_yy"].tolist(), locs["Migration start year"].tolist()]
plot elements¶
def limits(mimax, coors):
"""gets the minimum and maximum values for the x and y axes so we can zoom in"""
for i, v in enumerate(coors):
#ummmmmmmmmmmmmmmmmmm theoretically i could condense this code to make it less shit.
#and i will.
#at some point.
if i%2 == 0 and v < mimax[i]:
mimax[i] = v
elif i%2 != 0 and v > mimax[i]:
mimax[i] = v
return mimax
def startmonth(yrs):
"""gets the start month of the migration for each year-- for the legend"""
ms = []
for y in yrs:
ms.append(bf[(bf["Migration start year"]==y) &
(bf["Bird species"] == targetspecies.value) &
(bf["Migration nodes"] == "Origin")]["Migration start month"].unique())
return ms
renderers¶
def rendermap(unique_codes, specie, yrs):
"""re-renders the map"""
fig, ax = plt.subplots()
world.plot(ax=ax, color=random.choice(cswag.pastels), alpha=0.75)
mimax = [1000, -1000, 1000, -1000]
for i, code in enumerate(unique_codes):
rte = route(specie, code)
mimax = limits(mimax, [min(rte[0]), max(rte[0]), min(rte[1]), max(rte[1])])
try:
chosencolor = random.choice(cswag.plot_colors[cswag.rainbow[yrs.index(rte[2][0])%6]])
except:
#this is for that annoying fucking issue w years i talked abt in the updateyears
#func. we ball tho
chosencolor = "black"
ax.plot(rte[0], rte[1], marker='.', linewidth=1, markersize=3,color=chosencolor)
ax.scatter(rte[0][0], rte[1][0], marker='*',
s=20, color=chosencolor, label=f"Origin, Route{i}")
ax.scatter(rte[0][-1], rte[1][-1], marker='o',
s=20, color=chosencolor, label=f"End, Route{i}")
#zooms in like 2 the main parts of the route. idk i might change it to +-0.5
ax.set_ylim(mimax[2]-0.5, mimax[3]+0.5)
ax.set_xlim(mimax[0]-0.5, mimax[1]+0.5)
#we make it pink and such
yassify(ax, specie)
fig.tight_layout();
plt.close(fig)
return fig
def maphv(unique_codes, specie, yrs):
"""this renders the map but on holoviz so it's not static. didnt help the rendering
speed issue tho
lmao"""
rteplots = []
mimax = [1000, -1000, 1000, -1000]
for i, code in enumerate(unique_codes):
rt = pd.DataFrame(np.column_stack(route(specie, code)),
columns=["x", "y", "ye"])
#this is to labelj if the stops are origns or destinations otherwise itll put
#transit
stops = {0:"Origin", len(rt):"Destination"}
#usign a ternary operator + a dictionary bc i wanted a ternary um idk but couldn't do
#three options w it???? idk this feels like the most memory inefficient way to possibly
#do this so idk. we'll see
#also this is for making the label that will hover
rt["hover"] = [f"Year: {rt["ye"][p]:.0f}\n{stops[p]
if (p == 0) or (p == len(rt)) else "Transit"}" for p in range(len(rt))]
mimax = limits(mimax, [min(rt["x"]), max(rt["x"]), min(rt["y"]), max(rt["y"])])
#therse some fucking issue with years occasionally being slightly different due to
#time passing and year overlap and such. idk. i need to figure out a less cooked
#solution than this. thsi is temperorary.
try:
chosencolor = random.choice(cswag.plot_colors[cswag.rainbow[yrs.index(int(rt["ye"][0]))%6]])
except:
chosencolor = "black"
#ok so this needs both hover_cols and hover_tooltips to work. hover_col is set
#as the columnthat i custom made earlier and it also needs hover_tooltips to make th e
#additional lable of x y diseppear. idk. lol
rteplot = rt.hvplot.points(x="x", y="y", projection = ccrs.PlateCarree(), geo=True,
color=chosencolor,
hover_cols=["hover"]).opts(hover_tooltips=[("", "@hover")])
rteplots.append(rteplot)
#OK. so this is cloning world & custom resizing in the clone itself
mp = world.hvplot.polygons(color=GLOBAL_PLTCOLORZ[specie], xlim=(mimax[0]-0.5, mimax[1]+0.5),
ylim=(mimax[2]-0.5, mimax[3]+0.5), alpha=0.7,
projection = ccrs.PlateCarree())
#THE ONLY WAY that this overlay will ascutlly fucking work
#is if you set the same projection
#for both being PlateCarree. its so evil bro.
for r in rteplots:
mp = mp * r
return mp
def update_yrs_blrb_pic(event):
"""this modifies the checkbox for years, when the bird changes, to show only the years that
we have data for that bird. also it updates the picture """
#NOTE: sometimes some weird shit happens where the same route can have different
#migration start dates so
#uhhhhhhhhh
#i'll prolly change to a range when i have more time to gaf about such things
new_species = event.new
new_years= sorted(int(y) for y in pd.unique(
bf.loc[bf['Bird species'] == new_species, 'Migration start year']
).tolist())
#this is not just a years updater any more
#nts change function name to reflect
blurblabels.value = bs.get_blurb(new_species)
birb.object = f"bird_pics/{new_species}.jpg"
yearcheck.options = new_years
conservation = bf.loc[bf['Bird species'] == new_species,
'The IUCN Red List (2023)'].tolist()[0]
bitext.object = conservation
#i made conservation colors for each one its in a dictionary in bridspecies.py so like
#if it was critically endangered it'd be red.
#then the box of the legend switches to a gradient
#between pink and that color.
legendbox.styles = {'background': f"linear-gradient(135deg, #FF9EB3, {sp_info[conservation][1]}"}
constatus.value = [bicon, bitext]
yearcheck.value = [new_years[0]]
def update_legend(event):
"""updates the legend!!!!!!!!!!!!!! *the object itself
by calling a markdown update function"""
newyrs = event.new
leglabels.object = superultramd(newyrs)
local aesthetic stuff¶
def yassify(ax, species):
"""yassifies the graph idk it needed to be cuntier"""
ax.set_title(f"Routes for {species}", color=GLOBAL_PINK)
ax.set_xlabel("Longitude", color=GLOBAL_PINK)
ax.set_ylabel("Latitude", color=GLOBAL_PINK)
ax.tick_params(axis='x', color=GLOBAL_PINK)
ax.tick_params(axis='y', color=GLOBAL_PINK)
for spine in ax.spines.values():
spine.set_edgecolor(pink)
spine.set_color(pink)
spine.set_linewidth(1.2)
ax.grid(True, alpha=0.2, linestyle="--")
def superultramd(yrs):
"""edits table data for the legend on the side of the plot."""
#we clone the global markdown which is pulled form the .txt doc at the beginning
mdwn = GLOBAL_TABLE_MARKDOWN
stmonths = startmonth(yrs)
#we iteratively added the lines about months, yr etc for each plot that gets created.
for i, y in enumerate(yrs):
mdwn += f"\n| {y} | {cswag.rainbow[i%6]} | {(", ".join([GLOBAL_MONTHZ[x] for x in stmonths[i]]))} |"
return mdwn
display objects¶
#INITIALIZING ALL THE OBJECTS FOR THE DISPLAY.
#note these are individual objects not yet in columns yet.
#the columnifying will come later
targetspecies = pn.widgets.Select(options=allspecies, value=allspecies[0])
yearcheck = pn.widgets.CheckBoxGroup(name='Years', value=[2001], options=[])
blurblabels = pn.widgets.indicators.String(value="Info:", font_size= "16pt")
leglabels = pn.pane.Markdown(object=superultramd([]))
bicon = pn.pane.SVG('bird.svg', width=150, css_classes=["birb"])
bitext = pn.pane.Markdown('', styles={'font_size':'26pt', 'color':'white'})
constatus = pn.GridBox(*[bicon, bitext], ncols=2, nrows=1)
birb = pn.pane.Image(f"bird_pics/{targetspecies.value}.jpg")
#watcher(s) for changes in our species. THIS IS A HUGE FUCKING PAIN IN THE ASS.
#IF THE APP CRASHES IT'S PROBABLY DUE TO THIS. JUST CLEAR ALL OUTPUTS AND RERUN.
targetspecies.param.watch(update_yrs_blrb_pic, ["value"], onlychanged=True)
yearcheck.param.watch(update_legend, ["value"], onlychanged=True);
object depends¶
#so ive done a couple different dependency things w panel. #1 is a watcher whicch
#u can see above. also just a @pn.depends thing which is what we learned in class.
#idk how efficient it is mixing them. overall not thrilled
#w/ the panel app... it's usable but laggy. idk. we'll see.
@pn.depends(targetspecies.param.value, yearcheck.param.value)
def yearfilterable(species, yrs):
"""allows you to filter the routes by year and graph accordingly."""
#making a smaller dataset based on the years... yf... idk it stands for yearframe i guess
specie = species[0] if isinstance(species, (list, tuple, np.ndarray, pd.Series)) else species
yf = bf[(bf['Bird species'] == specie) & (bf['Migration start year'].isin(yrs))]
unique_codes = yf[yf['Bird species'] == specie]["Migratory route codes"].unique()
#fig will get resized according to dimensions but this gives it a good baseline
#to start w so we end up a w larger graph
#closing the previous plot to save space and such
plt.close()
return(maphv(unique_codes, specie, yrs))
rows¶
#stack bird species ontop of its corresponding pic
species_col = pn.Column("# BIRD SPECIES", targetspecies, birb, width=400, css_classes=["sidebar"]);
#yrs and blurb are in the same row aswell.
years_col = pn.Column("# YEAR(S)", yearcheck, width=180, css_classes=["sidebar"]);
blurb = pn.Column("# ADDITIONAL INFO", blurblabels, css_classes=["blurb", "sidebar"]);
legendbox = pn.Column(leglabels, constatus, css_classes=["sidebar"])
row1 = pn.Row(
species_col,
years_col,
blurb,
sizing_mode="stretch_width"
);
row2 = pn.Row(
legendbox,
pn.Column(yearfilterable, sizing_mode="stretch_width"),
sizing_mode="stretch_width"
);
creates the overall app¶
#initializes in a new tab. i'll make it servable at some point so u can run from cmd.
app = pn.Column(
row1,
row2,
width=1000,
sizing_mode="stretch_height"
)
runs the app¶
update_yrs_blrb_pic(type('e', (), {'new': targetspecies.value})())
update_legend(type('e', (), {'new': yearcheck.value})())
app.show(title="SWAG");
Launching server at http://localhost:34223